use crate::dom;
use std::collections::HashMap;

pub fn parse(source: String) -> dom::Node {
  let mut nodes = Parser {
    pos: 0,
    input: source,
  }
  .parse_nodes();

  if nodes.len() == 1 {
    nodes.swap_remove(0)
  } else {
    dom::elem("html".to_string(), HashMap::new(), nodes)
  }
}

#[cfg(test)]
mod test {
  use super::*;
  #[test]
  fn parse_single_tag() {
    let root = parse("<a>a test</a>".to_string());
    assert_eq!(root.children.len(), 1);
    let text_node = &root.children[0];
    assert_eq!(text_node.children.len(), 0);

    if let dom::NodeType::Text(text) = &text_node.node_type {
      assert_eq!(text, "a test");
    }
  }

  #[test]
  fn parse_tag_with_attributes() {
    let root = parse("<a class='classa classb'></a>".to_string());
    assert_eq!(root.children.len(), 0);

    if let dom::NodeType::Element(el) = &root.node_type {
      assert_eq!(el.tag_name, "a");
      assert_eq!(el.attributes["class"], "classa classb");
    }
  }

  #[test]
  fn parse_nested_tag() {
    let root = parse("<a><b><c></c></b></a>".to_string());
    assert_eq!(root.children.len(), 1);
    let c = dom::elem("c".to_string(), dom::AttrMap::new(), Vec::new());
    let b = dom::elem("b".to_string(), dom::AttrMap::new(), vec![c]);
    let a = dom::elem("a".to_string(), dom::AttrMap::new(), vec![b]);
    assert_eq!(root, a);
  }
}

struct Parser {
  pos: usize,
  input: String,
}

impl Parser {
  fn parse_nodes(&mut self) -> Vec<dom::Node> {
    let mut nodes = vec![];
    loop {
      self.consume_whitespace();
      if self.eof() || self.starts_with("</") {
        break;
      }
      nodes.push(self.parse_node())
    }
    nodes
  }

  fn parse_node(&mut self) -> dom::Node {
    match self.next_char() {
      '<' => self.parse_element(),
      _ => self.parse_text(),
    }
  }

  fn parse_element(&mut self) -> dom::Node {
    assert_eq!(self.consume_char(), '<');
    let tag_name = self.parse_tag_name();
    let attrs = self.parse_attributes();
    assert_eq!(self.consume_char(), '>');

    let children = self.parse_nodes();

    assert_eq!(self.consume_char(), '<');
    assert_eq!(self.consume_char(), '/');
    assert_eq!(self.parse_tag_name(), tag_name);
    assert_eq!(self.consume_char(), '>');

    dom::elem(tag_name, attrs, children)
  }

  fn parse_tag_name(&mut self) -> String {
    self.consume_while(|c| match c {
      'a'..='z' | 'A'..='Z' | '0'..='9' => true,
      _ => false,
    })
  }

  fn parse_attributes(&mut self) -> dom::AttrMap {
    let mut attributes = dom::AttrMap::new();
    loop {
      self.consume_whitespace();
      if self.next_char() == '>' {
        break;
      }
      let (name, value) = self.parse_attr();
      attributes.insert(name, value);
    }
    attributes
  }

  fn parse_attr(&mut self) -> (String, String) {
    let name = self.parse_tag_name();
    assert_eq!(self.consume_char(), '=');
    let value = self.parse_attr_value();
    (name, value)
  }

  fn parse_attr_value(&mut self) -> String {
    let open_quote = self.consume_char();
    assert!(open_quote == '"' || open_quote == '\'');
    let value = self.consume_while(|c| c != open_quote);
    assert_eq!(self.consume_char(), open_quote);
    value
  }

  fn parse_text(&mut self) -> dom::Node {
    dom::text(self.consume_while(|c| c != '<'))
  }

  fn consume_whitespace(&mut self) {
    self.consume_while(char::is_whitespace);
  }

  fn consume_char(&mut self) -> char {
    let mut iter = self.input[self.pos..].char_indices();
    let (_, cur_char) = iter.next().unwrap();
    let (next_pos, _) = iter.next().unwrap_or((1, ' '));
    self.pos += next_pos;
    cur_char
  }

  fn consume_while<F>(&mut self, test: F) -> String
  where
    F: Fn(char) -> bool,
  {
    let mut result = String::new();
    while !self.eof() && test(self.next_char()) {
      result.push(self.consume_char());
    }
    result
  }

  fn next_char(&self) -> char {
    self.input[self.pos..].chars().next().unwrap()
  }

  fn starts_with(&self, s: &str) -> bool {
    self.input[self.pos..].starts_with(s)
  }

  fn eof(&self) -> bool {
    self.pos >= self.input.len()
  }
}
